// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

//
// OutputStep.cs
//
// Author:
//   Jb Evain (jbevain@gmail.com)
//
// (C) 2006 Jb Evain
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using ILLink.Shared;
using Mono.Cecil;

namespace Mono.Linker.Steps
{

    public class OutputStep : BaseStep
    {
        readonly List<string> assembliesWritten;

        public OutputStep()
        {
            assembliesWritten = new List<string>();
        }

        protected override bool ConditionToProcess()
        {
            return Context.ErrorsCount == 0;
        }

        protected override void Process()
        {
            CheckOutputDirectory();
            OutputPInvokes();
            Tracer.Finish();
        }

        protected override void EndProcess()
        {
            if (Context.AssemblyListFile != null)
            {
                using (var w = File.CreateText(Context.AssemblyListFile))
                {
                    w.WriteLine("[" + string.Join(", ", assembliesWritten.Select(a => "\"" + a + "\"").ToArray()) + "]");
                }
            }
        }

        void CheckOutputDirectory()
        {
            if (Directory.Exists(Context.OutputDirectory))
                return;

            Directory.CreateDirectory(Context.OutputDirectory);
        }

        protected override void ProcessAssembly(AssemblyDefinition assembly)
        {
            OutputAssembly(assembly);
        }

        protected void WriteAssembly(AssemblyDefinition assembly, string directory)
        {
            WriteAssembly(assembly, directory, SaveSymbols(assembly));
        }

        protected virtual void WriteAssembly(AssemblyDefinition assembly, string directory, WriterParameters writerParameters)
        {
            foreach (var module in assembly.Modules)
            {
                // Write back pure IL even for crossgen-ed assemblies
                if (module.IsCrossgened())
                {
                    module.Attributes |= ModuleAttributes.ILOnly;
                    module.Attributes ^= ModuleAttributes.ILLibrary;
                    module.Architecture = TargetArchitecture.I386; // I386+ILOnly which ultimately translates to AnyCPU
                    module.Characteristics |= ModuleCharacteristics.NoSEH;
                }
            }

            string outputName = GetAssemblyFileName(assembly, directory);
            try
            {
                assembly.Write(outputName, writerParameters);
            }
            catch (Exception e)
            {
                // We should be okay catching everything here, assembly.Write is all in Cecil and most of the state necessary to debug will be captured in assembly
                throw new LinkerFatalErrorException(MessageContainer.CreateErrorMessage(null, DiagnosticId.FailedToWriteOutput, outputName), e);
            }
        }

        void OutputAssembly(AssemblyDefinition assembly)
        {
            string directory = Context.OutputDirectory;

            CopyConfigFileIfNeeded(assembly, directory);

            var action = Annotations.GetAction(assembly);
            Context.LogMessage($"Output action: '{action,8}' assembly: '{assembly}'.");

            switch (action)
            {
                case AssemblyAction.Save:
                case AssemblyAction.Link:
                case AssemblyAction.AddBypassNGen:
                    WriteAssembly(assembly, directory);
                    CopySatelliteAssembliesIfNeeded(assembly, directory);
                    assembliesWritten.Add(GetOriginalAssemblyFileInfo(assembly).Name);
                    break;
                case AssemblyAction.Copy:
                    CloseSymbols(assembly);
                    CopyAssembly(assembly, directory);
                    CopySatelliteAssembliesIfNeeded(assembly, directory);
                    assembliesWritten.Add(GetOriginalAssemblyFileInfo(assembly).Name);
                    break;
                case AssemblyAction.Delete:
                    CloseSymbols(assembly);
                    DeleteAssembly(assembly, directory);
                    break;
                default:
                    CloseSymbols(assembly);
                    break;
            }
        }

        private void OutputPInvokes()
        {
            if (Context.PInvokesListFile == null)
                return;

            using (var fs = File.Open(Path.Combine(Context.OutputDirectory, Context.PInvokesListFile), FileMode.Create))
            {
                var values = Context.PInvokes.Distinct().OrderBy(l => l);
                // Ignore warning, since we're just enabling analyzer for dogfooding
#pragma warning disable IL2026
                var jsonSerializer = new DataContractJsonSerializer(typeof(List<PInvokeInfo>));
                jsonSerializer.WriteObject(fs, values);
#pragma warning restore IL2026
            }
        }

        protected virtual void DeleteAssembly(AssemblyDefinition assembly, string directory)
        {
            var target = GetAssemblyFileName(assembly, directory);
            if (File.Exists(target))
            {
                File.Delete(target);
                File.Delete(target + ".mdb");
                File.Delete(Path.ChangeExtension(target, "pdb"));
                File.Delete(GetConfigFile(target));
            }
        }

        void CloseSymbols(AssemblyDefinition assembly)
        {
            Annotations.CloseSymbolReader(assembly);
        }

        WriterParameters SaveSymbols(AssemblyDefinition assembly)
        {
            var parameters = new WriterParameters
            {
                DeterministicMvid = Context.DeterministicOutput
            };

            if (!Context.LinkSymbols)
                return parameters;

            if (!assembly.MainModule.HasSymbols)
                return parameters;

            // Use a string check to avoid a hard dependency on Mono.Cecil.Pdb
            if (Environment.OSVersion.Platform != PlatformID.Win32NT && assembly.MainModule.SymbolReader.GetType().FullName == "Mono.Cecil.Pdb.NativePdbReader")
                return parameters;

            parameters.WriteSymbols = true;
            parameters.SymbolWriterProvider = new CustomSymbolWriterProvider(Context.PreserveSymbolPaths);
            return parameters;
        }


        void CopySatelliteAssembliesIfNeeded(AssemblyDefinition assembly, string directory)
        {
            if (!Annotations.ProcessSatelliteAssemblies)
                return;

            FileInfo original = GetOriginalAssemblyFileInfo(assembly);
            string resourceFile = GetAssemblyResourceFileName(original.FullName);

            foreach (var subDirectory in Directory.EnumerateDirectories(original.DirectoryName!))
            {
                var satelliteAssembly = Path.Combine(subDirectory, resourceFile);
                if (!File.Exists(satelliteAssembly))
                    continue;

                string cultureName = subDirectory.Substring(subDirectory.LastIndexOf(Path.DirectorySeparatorChar) + 1);
                string culturePath = Path.Combine(directory, cultureName);

                Directory.CreateDirectory(culturePath);
                File.Copy(satelliteAssembly, Path.Combine(culturePath, resourceFile), true);
            }
        }

        void CopyConfigFileIfNeeded(AssemblyDefinition assembly, string directory)
        {
            string config = GetConfigFile(GetOriginalAssemblyFileInfo(assembly).FullName);
            if (!File.Exists(config))
                return;

            string target = Path.GetFullPath(GetConfigFile(GetAssemblyFileName(assembly, directory)));

            if (config == target)
                return;

            File.Copy(config, GetConfigFile(GetAssemblyFileName(assembly, directory)), true);
        }

        static string GetAssemblyResourceFileName(string assembly)
        {
            return Path.GetFileNameWithoutExtension(assembly) + ".resources.dll";
        }

        static string GetConfigFile(string assembly)
        {
            return assembly + ".config";
        }

        FileInfo GetOriginalAssemblyFileInfo(AssemblyDefinition assembly)
        {
            return new FileInfo(Context.GetAssemblyLocation(assembly));
        }

        protected virtual void CopyAssembly(AssemblyDefinition assembly, string directory)
        {
            FileInfo fi = GetOriginalAssemblyFileInfo(assembly);
            string target = Path.GetFullPath(Path.Combine(directory, fi.Name));
            string source = fi.FullName;

            if (source == target)
                return;

            File.Copy(source, target, true);
            if (!Context.LinkSymbols)
                return;

            var mdb = source + ".mdb";
            if (File.Exists(mdb))
                File.Copy(mdb, target + ".mdb", true);

            var pdb = Path.ChangeExtension(source, "pdb");
            if (File.Exists(pdb))
                File.Copy(pdb, Path.ChangeExtension(target, "pdb"), true);
        }

        protected virtual string GetAssemblyFileName(AssemblyDefinition assembly, string directory)
        {
            string file = GetOriginalAssemblyFileInfo(assembly).Name;
            return Path.Combine(directory, file);
        }
    }
}
